package org.melanesia.beans;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.melanesia.beans.exceptions.NoGetterFoundException;
import org.melanesia.beans.exceptions.NoSetterFoundException;


class AccessorFactory {
    
	// FIXME: this should be in some configuration file
	private final static int PROPERTY_ACCESSOR_CACHE_SIZE = 10000;
	
	private static Map<String, PropertyAccessor> PROPERTY_ACCESSOR_CACHE = new LinkedHashMap<String, PropertyAccessor>(PROPERTY_ACCESSOR_CACHE_SIZE, 0.75F, true) {

		private static final long serialVersionUID = -7569227739343284480L;

		public boolean removeEldestEntry (Map.Entry<String, PropertyAccessor> eldest){
			return size() > PROPERTY_ACCESSOR_CACHE_SIZE;
	    }
	};
	
    static PropertyAccessor createAccessor(Class<?> beanClass, String propertyName) {
    	
    	String cacheKey = beanClass.getName() + "***" + propertyName;
    	if (!PROPERTY_ACCESSOR_CACHE.containsKey(cacheKey)) {
    	
	        Setter setter = findSetter(beanClass, propertyName);
	        Getter getter = findGetter(beanClass, propertyName);
	        
	        if (setter == null) {
	        	throw new NoSetterFoundException("Could not create accessor for property \"" + propertyName + "\" on class \"" + beanClass + "\".");
	        }
	        
	        if (getter == null) {
	        	throw new NoGetterFoundException("Could not create accessor for property \"" + propertyName + "\" on class \"" + beanClass + "\".");
	        }
	        
	        PROPERTY_ACCESSOR_CACHE.put(cacheKey, new PropertyAccessor(propertyName, setter, getter));
	        
    	}
    	
        return PROPERTY_ACCESSOR_CACHE.get(cacheKey); 
    }
    
    private static Setter findSetter(Class<?> beanClass, String propertyName) {
        
        Setter setter = null;
        
        // Check special types first
        if (BeanUtils.isAssignableFrom(Map.class, beanClass)) {
            setter = new MapSetter(propertyName);
        } else {
        
            if (BeanUtils.isSimpleProperty(propertyName)) {
            
                // Simple property, just find setter
                setter = findMethodSetter(beanClass, propertyName);
                if (setter == null) {
                    setter = findFieldSetter(beanClass, propertyName);
                }
            
            } else {
                
                // Composite property, this is harder
                setter = findCompositeSetter(beanClass, propertyName);
            }
        }
           
        return setter;
    }
    
    private static Getter findGetter(Class<?> beanClass, String propertyName) {
        Getter getter = null;
        
        // Check special types first
        if (BeanUtils.isAssignableFrom(Map.class, beanClass)) {
            getter = new MapGetter(propertyName);
        } else {
            if (BeanUtils.isSimpleProperty(propertyName)) {
                // Simple property
                getter = findMethodGetter(beanClass, propertyName);
                
                if (getter == null) {
                    getter = findFieldGetter(beanClass, propertyName);
                }
            } else {
                // Composite property
                getter = findCompositeGetter(beanClass, propertyName);
            }
        }
            
        return getter;
    }

    private static FieldGetter findFieldGetter(Class<?> beanClass, String propertyName) {
        
        String setterFieldName = BeanUtils.sqlToCamelCase(propertyName);
        
        for (Field field : beanClass.getDeclaredFields()) {
            
            if (setterFieldName.equals(field.getName())) {
                return new FieldGetter(field);
            }
        }
        
        return null;
    } 
    
    private static FieldSetter findFieldSetter(Class<?> beanClass, String propertyName) {
        
        String setterFieldName = BeanUtils.sqlToCamelCase(propertyName);
        
        for (Field field : beanClass.getDeclaredFields()) {
            if (setterFieldName.equals(field.getName())) {
                return new FieldSetter(field);
            }
        }
        
        return null;
    }   
    
    private static MethodGetter findMethodGetter(Class<?> beanClass, String propertyName) {
    	
        String getterMethodName1 = "get" + BeanUtils.capitalize(BeanUtils.sqlToCamelCase(propertyName));
        String getterMethodName2 = "is" + BeanUtils.capitalize(BeanUtils.sqlToCamelCase(propertyName));
        
        for (Method method : beanClass.getDeclaredMethods()) {
        
            if (getterMethodName1.equals(method.getName()) || getterMethodName2.equals(method.getName())) {
            	return new MethodGetter(method);
            }
        }
        
        return null;
    }   
    
    
    private static MethodSetter findMethodSetter(Class<?> beanClass, String propertyName) {
        
        String setterMethodName = "set" + BeanUtils.capitalize(BeanUtils.sqlToCamelCase(propertyName));
        
        for (Method method : beanClass.getDeclaredMethods()) {
            
            if (setterMethodName.equals(method.getName())) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                if (parameterTypes != null && parameterTypes.length == 1) {
                    return new MethodSetter(method);
                }
            }
        }
        
        return null;
    }
    
    private static CompositeGetter findCompositeGetter(Class<?> beanClass, String propertyName) {
        List<PropertyAccessor> list = new ArrayList<PropertyAccessor>();
        fillAccessorChain(list, beanClass, propertyName);
        
        return new CompositeGetter(list);
    }
    
    private static CompositeSetter findCompositeSetter(Class<?> beanClass, String propertyName) {

        List<PropertyAccessor> list = new ArrayList<PropertyAccessor>();
        fillAccessorChain(list, beanClass, propertyName);
        
        return new CompositeSetter(list);
    }
    
    private static void fillAccessorChain(List<PropertyAccessor> accessorChain, Class<?> beanClass, String propertyName) {
        
        if (BeanUtils.isSimpleProperty(propertyName)) {
            
            // finally last bean
            PropertyAccessor accessor = createAccessor(beanClass, propertyName);
            accessorChain.add(accessor);
            
        } else {
           
            String simplePropertyName = BeanUtils.getSimpleProperty(propertyName);
            String subPropertyName = BeanUtils.getSubProperty(propertyName);
            
            PropertyAccessor accessor = createAccessor(beanClass, simplePropertyName);
            accessorChain.add(accessor);
            
            // Final-recurrent call to create getters for rest of the beans
            fillAccessorChain(accessorChain, accessor.getPropertyClass(), subPropertyName);
        }
    }
}
